Day 26 的測試跑在你的機器上,也就只有你看得到。今天把它推進 GitHub Actions,每次 push 都跑;同時把紅燈變成別人也讀得懂的東西:report、trace、screenshot 都要撈得回來。
本機跑測試有兩個問題,而且都不是「麻煩」而已。
第一,它只在你想到的時候跑。改完一段程式碼、趕著下班的那次,通常就是最該跑的那次。
第二,也是更嚴重的,本機的綠燈不能拿來說服任何人。你說「我這邊是綠的」,對方無從查證,也無從重播。CI 的價值不只是自動化,是它產生了一份別人可以檢視的紀錄。
但只把測試搬上去還不夠。CI 紅了,你能拿到的預設訊息可能只有一行 exit code 1,或一段沒有上下文的逾時。那種紅燈跟沒有紅燈差不多,因為沒有人能從它往下走。
所以這一天有兩件事:把測試接上去,以及讓紅燈自帶證據。第二件事的規格從 Day 8 到 Day 11 就立好了,今天是把它搬進 pipeline。
workflow 的核心是 matrix,同一份測試碼同時跑兩個環境:
name: e2e
on:
push:
branches: ['**']
paths: ['tests/**', 'package.json', '.github/workflows/e2e.yml']
pull_request:
workflow_dispatch:
inputs:
repeat_flaky:
description: '額外跑 flaky 量測(重複次數,0 = 不跑)'
default: '0'
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: e2e (${{ matrix.sut }})
strategy:
fail-fast: false
matrix:
sut: [clean, with-bugs]
fail-fast: false 是關鍵。預設值會在第一格紅掉時取消其他格,那正好毀掉這條 pipeline 的用途:我們要的就是「clean 綠、with-bugs 紅」這個對照,少一邊就判不出來。
實跑五次的結果:
| run | e2e (clean) |
e2e (with-bugs) |
|---|---|---|
30710272941 |
✗ 紅 | ✗ 紅 |
30710272941(rerun --failed) |
✗ 紅 | ✗ 紅 |
30710677728 |
✓ 3 passed | ✗ 紅 |
30710750654 |
✓ | 未跑 |
30710799963 |
✓ | 未跑 |

with-bugs 一路紅是設計,它紅在斷言(Received: 0),不是紅在別的地方。clean 前兩次紅不是設計,那是真的踩到坑,整個過程是 Day 28 的主角。
順帶一提,clean 是 3 passed 不是 4:登入測試在 CI 上自動 skip,因為 repo 沒設帳密 secrets。祕密不落地的代價是少跑一支,這個取捨要寫在明處,不要讓人以為測試少了一支是漏掉。
紅燈能不能往下走,取決於 CI 有沒有把證據留下來。上傳 artifact 的條件是 if: always(),因為紅的時候才最需要它。
真正的重點在名稱。artifact 名不是隨手取的,是一份契約:下游要靠名稱才分得出這次紅的是哪一層。UI 測試與 API 測試的結果分開命名,否則解析的人拿到一包混在一起的檔案,只能自己猜。
改名要連契約表一起改,這條跟 Day 11 的證據包命名是同一個道理:能被自動處理的前提是名字可預測。
拉證據的指令固定兩條:
gh run view <run-id> --log-failed # 只看失敗那幾行
gh run download <run-id> # 把 artifact 撈下來
--log-failed 那條很重要。整份 CI log 動輒幾萬行,全讀進 context 是純粹的浪費,而失敗訊息就那幾十行。
觸發時機分三種,不要混成一種。 PR 要快,跑必要範圍;nightly 跑全量;workflow_dispatch 留給手動重跑與參數化實驗,本專案就是拿它做 flaky 量測。
API 測試切成獨立 job,而且不裝瀏覽器。 安裝瀏覽器通常是整條裡最慢的一步,API 測試不需要它。讓後端規則的紅燈在幾十秒內先亮,比等 UI 測試跑完十分鐘才知道有價值。
但這裡有個坑:UI job 掛 needs: api-test 只能用在 PR。 nightly 也掛的話,API 一紅整批 UI 會變成 skipped,隔天早上你看到的是一份缺一半的報告,而不是壞消息。
祕密只走 GitHub secrets,不寫死在 workflow、不進知識庫。
改 workflow 是副作用,先把 diff 完整列給人看,同意才寫檔。
先盤點專案的測試指令與設定,決定三種觸發時機;用 matrix 讓同一份測試碼跑兩個環境,fail-fast: false 保住對照;跑完不論綠紅都上傳 artifact,名稱照契約;紅了用 --log-failed 只讀失敗那幾行,需要證據再 download 撈下來;最後把結論交給下一天的分流。
push / PR / 手動
│
▼
盤點測試指令與設定
│
▼
matrix: [clean, with-bugs]
fail-fast: false
│ │
▼ ▼
e2e (clean) e2e (with-bugs)
│ │
└──────┬─────┘
▼
if: always() 上傳 artifact
(名稱照契約,UI 與 API 分開)
│
┌──────┴──────┐
全綠 有紅
│ │
▼ ▼
收工 gh run view --log-failed
gh run download
│
▼
交給明天的分流
CI 的價值是產生別人可以檢視的紀錄,不只是自動跑。fail-fast: false 保住的是判斷力,兩個環境的對照少一邊,就分不出測試的錯與產品的錯。而 artifact 名是契約,紅燈能不能往下走,取決於證據撈不撈得回來、分不分得出是哪一層。
CI 紅了。第一個反應通常是打開測試碼開始改,改到它變綠為止。那是最貴的錯。
明天先分流再動手。手上有一組乾淨的對照:同一份測試碼、同一台機器、同一分鐘,只換受測環境。看它在哪一邊紅,就分得出是測試自己壞掉,還是產品真的回歸。而上面那兩次 clean 的紅燈,會示範這個判斷可以錯得多離譜,以及是什麼把它拉回來。